前言:以前做过一个相机,当时使用的是OpenCV库来进行滤镜和图片的处理,当时发现滤镜处理的时间比较长,实时性还有待进一步提高,对于使用NDK对camera处理每一帧,算法必须要非常优化和简单,对于一些复杂算法,处理时间比较长的,就不太适合实时处理的滤镜,那么我们该怎么优化相机的滤镜和保存拍照的图片呢?当然是使用OpenGL和RS渲染脚本,比起使用ndk来处理每一帧的图片,OpenGL和RenderScript脚本处理的相当快,它们的运算都是使用GPU去渲染的,而OpenCV的处理速度取决于CPU的执行速度,下面我们将分别来介绍下如何使用OpenGL和RenderScript来渲染图片流。
工程地址:https://github.com/liweiping1314521/RiemannCamera/
一. 下面我们来看看整个摄像头如何用OPENGL处理滤镜的:
先看一下类的关系图:
先来看看CameraGLSurfaceView这个类
/**
* 我们使用GLSurfaceView来显示Camera中预览的数据,所有的滤镜的处理,都是利用OPENGL,然后渲染到GLSurfaceView上
* 继承至SurfaceView,它内嵌的surface专门负责OpenGL渲染,绘制功能由GLSurfaceView.Renderer完成
*/
public class CameraGLSurfaceView extends GLSurfaceView {
private static final String LOGTAG = "CameraGLSurfaceView";
//渲染器
private CameraGLRendererBase mRenderer;
public CameraGLSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray styledAttrs = getContext().obtainStyledAttributes(attrs, R.styleable.CameraBridgeViewBase);
int cameraIndex = styledAttrs.getInt(R.styleable.CameraBridgeViewBase_camera_id, -1);
styledAttrs.recycle();
if(android.os.Build.VERSION.SDK_INT >= 21) {
mRenderer = new Camera2Renderer(context, this);
} else {
mRenderer = new CameraRenderer(context, this);
}
setEGLContextClientVersion(2);
//设置渲染器
setRenderer(mRenderer);
/**
* RENDERMODE_CONTINUOUSLY模式就会一直Render,如果设置成RENDERMODE_WHEN_DIRTY,
* 就是当有数据时才rendered或者主动调用了GLSurfaceView的requestRender.默认是连续模式,
* 很显然Camera适合脏模式,一秒30帧,当有数据来时再渲染,RENDERMODE_WHEN_DIRTY时只有在
* 创建和调用requestRender()时才会刷新
* 这样不会让CPU一直处于高速运转状态,提高手机电池使用时间和软件整体性能
*/
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}
再来看看滤镜的核心类,所需的OPENGL的数据都在这里初始化,构造函数中直接创建了filterGroup,它是滤镜的操作类,我们目前每个滤镜都是2层显示的,初始化的时候就会addFilter两层滤镜,一层为原始的OES外部纹理,第二层是我们选择的滤镜:
/**
* 显示滤镜的核心类
*/
public abstract class CameraGLRendererBase implements GLSurfaceView.Renderer, SurfaceTexture.OnFrameAvailableListener {
protected final String TAG = "CameraGLRendererBase";
protected int mCameraWidth = -1, mCameraHeight = -1;
protected int mMaxCameraWidth = -1, mMaxCameraHeight = -1;
protected int mCameraIndex = Constant.CAMERA_ID_ANY;
protected CameraGLSurfaceView mView;
/*和SurfaceView不同的是,SurfaceTexture在接收图像流之后,不需要立即显示出来,SurfaceTexture不需要显示到屏幕上,
因此我们可以用SurfaceTexture接收来自camera的图像流,然后从SurfaceTexture中取得图像帧的拷贝进行处理,
处理完毕后再送给另一个SurfaceView或者GLSurfaceView用于显示即可*/
protected SurfaceTexture mSurfaceTexture;
......
//滤镜操作类
private FilterGroup filterGroup;
//摄像头原始预览数据
private OESFilter oesFilter;
//索引下标
protected int mFilterIndex = 0;
public CameraGLRendererBase(Context context, CameraGLSurfaceView view) {
mContext = context;
mView = view;
filterGroup = new FilterGroup();
oesFilter = new OESFilter(context);
//OES是原始的摄像头数据纹理,然后再添加滤镜纹理
// N+1个滤镜(其中第一个从外部纹理接收的无滤镜效果)
filterGroup.addFilter(oesFilter);
//索引下标为0,表示是原始数据,即滤镜保持原始数据,不做滤镜运算
filterGroup.addFilter(FilterFactory.createFilter(0, context));
}
再来看看CameraGLRendererBase的GLSurfaceView.Renderer和SurfaceTexture.OnFrameAvailableListener几个回调接口
/**
* SurfaceTexture.OnFrameAvailableListener 回调接口
* @param surfaceTexture
* 正因是RENDERMODE_WHEN_DIRTY所以就要告诉GLSurfaceView什么时候Render,
* 也就是啥时候进到onDrawFrame()这个函数里。
* SurfaceTexture.OnFrameAvailableListener这个接口就干了这么一件事,当有数据上来后会进到
* 这里,然后执行requestRender()。
*/
@Override
public synchronized void onFrameAvailable(SurfaceTexture surfaceTexture) {
mUpdateST = true;
//有新的数据来了,可以渲染了
mView.requestRender();
}
GLSurfaceView.Renderer的回调接口
/**
* GLSurfaceView.Renderer 回调接口, 初始化
* @param gl
* @param config
*/
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
Log.i(TAG, "onSurfaceCreated");
//初始化所有滤镜,一般都是初始化滤镜的顶点着色器和片段着色器
filterGroup.init();
}
/**
* GLSurfaceView.Renderer 回调接口,比如横竖屏切换
* @param gl
* @param surfaceWidth
* @param surfaceHeight
*/
@Override
public void onSurfaceChanged(GL10 gl, int surfaceWidth, int surfaceHeight) {
Log.i(TAG, "onSurfaceChanged ( " + surfaceWidth + " x " + surfaceHeight + ")");
mHaveSurface = true;
//更新surface状态
updateState();
//设置预览界面大小
setPreviewSize(surfaceWidth, surfaceHeight);
//设置OPENGL视窗大小及位置
GLES20.glViewport(0, 0, surfaceWidth, surfaceHeight);
//创建滤镜帧缓存的数据
filterGroup.onFilterChanged(surfaceWidth, surfaceHeight);
}
/**
* GLSurfaceView.Renderer 回调接口, 每帧更新
* @param gl
*/
@Override
public void onDrawFrame(GL10 gl) {
if (!mHaveFBO) {
return;
}
synchronized(this) {
//mUpdateST这个值设置是在每次有新的数据帧上来的时候设置为true,
//我们需要从图像中提取最近一帧,然后可以设置值为false,每次来新的帧数据调用一次
if (mUpdateST) {
//更新纹理图像为从图像流中提取的最近一帧
mSurfaceTexture.updateTexImage();
mUpdateST = false;
}
//OES是原始的摄像头数据纹理,然后再添加滤镜纹理
//N+1个滤镜(其中第一个从外部纹理接收的无滤镜效果)
filterGroup.onDrawFrame(oesFilter.getTextureId());
}
}
当进入界面的时候,会加载CameraGLSurfaceView的enableView函数
/**
* 渲染器enable
*/
public void enableView() {
mRenderer.enableView();
}
/**
* 渲染器disable
*/
public void disableView() {
mRenderer.disableView();
}
/**
* 销毁渲染器
*/
public void onDestory(){
mRenderer.destory();
}
即调用了CameraGLRendererBase的这几个函数
public synchronized void enableView() {
Log.d(TAG, "enableView");
mEnabled = true;
updateState();
}
public synchronized void disableView() {
Log.d(TAG, "disableView");
mEnabled = false;
updateState();
}
//更新状态
private void updateState() {
Log.d(TAG, "updateState mEnabled = " + mEnabled + ", mHaveSurface = " + mHaveSurface);
boolean willStart = mEnabled && mHaveSurface && mView.getVisibility() == View.VISIBLE;
if (willStart != mIsStarted) {
if(willStart) {
doStart();
} else {
doStop();
}
} else {
Log.d(TAG, "keeping State unchanged");
}
Log.d(TAG, "updateState end");
}
会触发我们开启相机预览,获取摄像头的预览数据
/**
* 开启相机预览
*/
protected synchronized void doStart() {
Log.d(TAG, "doStart");
initSurfaceTexture();
openCamera(mCameraIndex);
mIsStarted = true;
if(mCameraWidth > 0 && mCameraHeight > 0) {
//设置预览高度和高度
setPreviewSize(mCameraWidth, mCameraHeight);
}
}
protected void doStop() {
Log.d(TAG, "doStop");
synchronized(this) {
mUpdateST = false;
mIsStarted = false;
mHaveFBO = false;
closeCamera();
deleteSurfaceTexture();
}
}
再打开摄像头之前,会初始化SurfaceTexture,设置回调监听,这样当每一帧有新的数据上来的时候,就会调用requestRender函数,进而在每帧渲染的时候,使用mSurfaceTexture.updateTexImage()提取最近的一帧
/**
*初始化SurfaceTexture并监听回调
*/
private void initSurfaceTexture() {
Log.d(TAG, "initSurfaceTexture");
deleteSurfaceTexture();
mSurfaceTexture = new SurfaceTexture(oesFilter.getTextureId());
mSurfaceTexture.setOnFrameAvailableListener(this);
}
本篇文章中,我们并不打算讲解Camera是如何使用的,我们只学习如何利用OPENGL处理滤镜,我们再回到CameraGLRendererBase的构造函数
public CameraGLRendererBase(Context context, CameraGLSurfaceView view) {
mContext = context;
mView = view;
filterGroup = new FilterGroup();
oesFilter = new OESFilter(context);
//OES是原始的摄像头数据纹理,然后再添加滤镜纹理
// N+1个滤镜(其中第一个从外部纹理接收的无滤镜效果)
filterGroup.addFilter(oesFilter);
//索引下标为0,表示是原始数据,即滤镜保持原始数据,不做滤镜运算
filterGroup.addFilter(FilterFactory.createFilter(0, context));
}
看看FilterGroup是如何工作的,通过上面的UML图,我们可知它集成抽象类AbstractFilter
/**
* 所有滤镜的操作类,所有的滤镜会在这里添加,比如,切换滤镜,添加滤镜
*/
public class FilterGroup extends AbstractFilter{
private static final String TAG = "FilterGroup";
//所有滤镜会保存在这个链表中
protected List<AbstractFilter> filters;
private int[] FBO = null;
private int[] texture = null;
protected boolean isRunning;
public FilterGroup() {
super(TAG);
filters = new ArrayList<>();
}
再来看看filterGroup.addFilter的函数
public void addFilter(final AbstractFilter filter){
if (filter == null) {
return;
}
if (!isRunning){
filters.add(filter);
} else {
addPreDrawTask(new Runnable() {
@Override
public void run() {
//由于执行runnable是在onDrawFrame中运行,当切换滤镜后,必须先初始化滤镜,然后添加到滤镜链表,
//再调用filterchange创建帧缓冲,bind纹理
filter.init();
filters.add(filter);
onFilterChanged(surfaceWidth, surfaceHeight);
}
});
}
}
switchFilter这个函数是在我们UI上切换滤镜的时候调用的,我们看看到底做了哪些操作呢?
/**
* 切换滤镜,切换滤镜的过程是这样的:
* 1.当摄像头没有运行的时候,直接添加;
* 2.当摄像头在运行的时候,先销毁最末尾的的滤镜,然后添加新的滤镜,并告知滤镜变化了,
* 帧缓存的数据必须也要做相应的调整
* @param filter
*/
public void switchFilter(final AbstractFilter filter){
if (filter == null) {
return;
}
if (!isRunning){
if(filters.size() > 0) {
filters.remove(filters.size() - 1).destroy();
}
filters.add(filter);
} else {
addPreDrawTask(new Runnable() {
@Override
public void run() {
if (filters.size() > 0) {
filters.remove(filters.size() - 1).destroy();
}
//由于执行runnable是在onDrawFrame中运行,当切换滤镜后,必须先初始化滤镜,然后添加到滤镜链表,
//再调用filterchange创建帧缓冲,bind纹理
filter.init();
filters.add(filter);
onFilterChanged(surfaceWidth, surfaceHeight);
}
});
}
}
下面我们再看看AbstractFilter里面做了啥
/**
* 抽象公共类,滤镜用到的所有数据结构都在这里完成
*
* opengl作为本地系统库,运行在本地环境,应用层的JAVA代码运行在Dalvik虚拟机上面
* android应用层的代码运行环境和opengl运行的环境不同,如何通信呢?
* 一是通过NDK去调用OPENGL接口,二是通过JAVA层封装好的类直接使用OPENGL接口,实际上它也是一个NDK,
* 但是使用这些接口就必须用到JAVA层中特殊的类,比如FloatBuffer
* 它为我们分配OPENGL环境中所使用的本地内存块,而不是使用JAVA虚拟机中的内存,因为OPGNGL不是运行在JAVA虚拟机中的
*
*/
public abstract class AbstractFilter {
private static final String TAG = "AbstractFilter";
private String filterTag;
protected int surfaceWidth, surfaceHeight;
protected FloatBuffer vert, texOES, tex2D, texOESFont;
//顶点着色器使用
protected final float vertices[] = {
-1, -1,
-1, 1,
1, -1,
1, 1 };
//片段着色器纹理坐标 OES 后置相机
protected final float texCoordOES[] = {
1, 1,
0, 1,
1, 0,
0, 0 };
//片段着色器纹理坐标
private final float texCoord2D[] = {
0, 0,
0, 1,
1, 0,
1, 1 };
//片段着色器纹理坐标 前置相机
private final float texCoordOESFont[] = {
0, 1,
1, 1,
0, 0,
1, 0 };
public AbstractFilter(String filterTag){
this.filterTag = filterTag;
mPreDrawTaskList = new LinkedList<>();
int bytes = vertices.length * Float.SIZE / Byte.SIZE;
//allocateDirect分配本地内存,order按照本地字节序组织内容,asFloatBuffer我们不想操作单独的字节,而是想操作浮点数
vert = ByteBuffer.allocateDirect(bytes).order(ByteOrder.nativeOrder()).asFloatBuffer();
texOES = ByteBuffer.allocateDirect(bytes).order(ByteOrder.nativeOrder()).asFloatBuffer();
tex2D = ByteBuffer.allocateDirect(bytes).order(ByteOrder.nativeOrder()).asFloatBuffer();
texOESFont = ByteBuffer.allocateDirect(bytes).order(ByteOrder.nativeOrder()).asFloatBuffer();
//put数据,将实际顶点坐标传入buffer,position将游标置为0,否则会从最后一次put的下一个位置读取
vert.put(vertices).position(0);
texOES.put(texCoordOES).position(0);
tex2D.put(texCoord2D).position(0);
texOESFont.put(texCoordOESFont).position(0);
String strGLVersion = GLES20.glGetString(GLES20.GL_VERSION);
if (strGLVersion != null) {
Log.i(TAG, "OpenGL ES version: " + strGLVersion);
}
}
······
public void onPreDrawElements(){
//清除颜色
GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
//清除屏幕
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
}
······
protected void draw(){
//使用顶点索引法来绘制
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
GLES20.glFlush();
}
abstract public void init();
abstract public void onDrawFrame(final int textureId);
abstract public void destroy();
//从链表中取出,由于链表里面保存的都是一个个runnable,即取出来运行起来
public void runPreDrawTasks() {
while (!mPreDrawTaskList.isEmpty()) {
mPreDrawTaskList.removeFirst().run();
}
}
//添加要执行的runnable到链表
public void addPreDrawTask(final Runnable runnable) {
synchronized (mPreDrawTaskList) {
mPreDrawTaskList.addLast(runnable);
}
}
下面我们进入滤镜的世界,看滤镜是如何实现的,先来看看原始的滤镜效果OESFilter
public class OESFilter extends AbstractFilter{
private static final String TAG = "OESFilter";
private Context mContext;
private String cameraVs, cameraFs;
private int progOES = -1;
private int vPosOES, vTCOES;
private int[] cameraTexture = null;
private int mCameraId = Constant.CAMERA_ID_ANY;
private int mOldCameraId = Constant.CAMERA_ID_ANY;
public OESFilter(Context context){
super(TAG);
mContext = context;
cameraTexture = new int[1];
}
@Override
public void init() {
//初始化着色器
initOESShader();
//初始化纹理
loadTexOES();
}
/**
*
*/
private void initOESShader(){
//读取顶点做色器
cameraVs = TextResourceReader.readTextFileFromResource(mContext, R.raw.camera_oes_vs);
//读取片段着色器
cameraFs = TextResourceReader.readTextFileFromResource(mContext, R.raw.camera_oes_fs);
//载入顶点着色器和片段着色器
progOES = Util.loadShader(cameraVs, cameraFs);
//获取顶点着色器中attribute location属性vPosition, vTexCoord
vPosOES = GLES20.glGetAttribLocation(progOES, "vPosition");
vTCOES = GLES20.glGetAttribLocation(progOES, "vTexCoord");
//开启顶点属性数组
GLES20.glEnableVertexAttribArray(vPosOES);
GLES20.glEnableVertexAttribArray(vTCOES);
}
private void loadTexOES() {
//生成一个纹理
GLES20.glGenTextures(1, cameraTexture, 0);
//绑定纹理,值得注意的是,纹理绑定的目标(target)并不是通常的GL_TEXTURE_2D,而是GL_TEXTURE_EXTERNAL_OES,
//这是因为Camera使用的输出texture是一种特殊的格式。同样的,在shader中我们也必须使用SamperExternalOES 的变量类型来访问该纹理。
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, cameraTexture[0]);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST);
}
再来看看加载着色器和显示纹理
@Override
public void onPreDrawElements() {
super.onPreDrawElements();
//加载着色器
GLES20.glUseProgram(progOES);
//关联属性与顶点数据的数组,告诉OPENGL再缓冲区vert中0的位置读取数据
GLES20.glVertexAttribPointer(vPosOES, 2, GLES20.GL_FLOAT, false, 4 * 2, vert);
if (mCameraId == Constant.CAMERA_ID_FRONT) {
GLES20.glVertexAttribPointer(vTCOES, 2, GLES20.GL_FLOAT, false, 4 * 2, texOESFont);
} else {
GLES20.glVertexAttribPointer(vTCOES, 2, GLES20.GL_FLOAT, false, 4 * 2, texOES);
}
}
@Override
public void onDrawFrame(int textureId) {
if (mOldCameraId == mCameraId) {
onPreDrawElements();
//设置窗口可视区域
GLES20.glViewport(0, 0, surfaceWidth, surfaceHeight);
//激活纹理,当有多个纹理的时候,可以依次递增GLES20.GL_TEXTUREi
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
//绑定纹理
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, cameraTexture[0]);
//设置sampler2D"sTexture1"到纹理 unit 0
GLES20.glUniform1i(GLES20.glGetUniformLocation(progOES, "sTexture"), 0);
//绘制
draw();
}
mOldCameraId = mCameraId;
}
我们看看OESFilter顶点着色器和片段着色器是如何写的?其实就是以2D纹理的方式绘制,坐标启用x和y坐标
camera_oes_vs.txt
attribute vec2 vPosition;
attribute vec2 vTexCoord;
varying vec2 texCoord;
void main() {
texCoord = vTexCoord;
gl_Position = vec4 ( vPosition.x, vPosition.y, 0.0, 1.0 );
}
camera_oes_fs.txt
#extension GL_OES_EGL_image_external : require
precision mediump float;
uniform samplerExternalOES sTexture;
varying vec2 texCoord;
void main() {
gl_FragColor = texture2D(sTexture,texCoord);
}
上面的注释已经很详细了,告诉了我们如何加载顶点和片段着色器,以及如何获取顶点着色器的属性,如何生成一个纹理,我们这里着重强调下,摄像头的纹理加载跟一般的纹理加载是不同的,一般我们绑定纹理使用GL_TEXTURE_2D这个属性,比如我们显示一张图片,我们直接用GL_TEXTURE_2D绑定就行了,但是摄像头不一样,它的纹理绑定的目标(并不是通常的GL_TEXTURE_2D,而是GL_TEXTURE_EXTERNAL_OES,这是因为Camera使用的输出texture是一种特殊的格式,它是通过SurfaceTexture来获取到摄像头数据的,同样的,在片段着色器中,我们也必须使用SamperExternalOES 的变量类型来访问该纹理,一般如果我们使用GL_TEXTURE_2D的话,我们只要使用sampler2D这个变量来访问即可 ,我们可以比较下camera_oes_fs着色器和其他片段着色器的不同。
这里,我们再额外的讲一下这里为啥会出现前置摄像头和后置摄像头的纹理不一样,及后置摄像头的纹理坐标为texOES,前置摄像头的纹理坐标为texOESFont,他们不一样都是相对应于顶点坐标的。
@Override
public void onPreDrawElements() {
super.onPreDrawElements();
//加载着色器
GLES20.glUseProgram(progOES);
//关联属性与顶点数据的数组,告诉OPENGL再缓冲区vert中0的位置读取数据
GLES20.glVertexAttribPointer(vPosOES, 2, GLES20.GL_FLOAT, false, 4 * 2, vert);
if (mCameraId == Constant.CAMERA_ID_FRONT) {
GLES20.glVertexAttribPointer(vTCOES, 2, GLES20.GL_FLOAT, false, 4 * 2, texOESFont);
} else {
GLES20.glVertexAttribPointer(vTCOES, 2, GLES20.GL_FLOAT, false, 4 * 2, texOES);
}
}
我们来看看android中的顶点坐标与纹理坐标的关系
我们再绘制顶点坐标的时候
顶点坐标跟纹理坐标
看了上面的解释,我们就知道为啥要这样显示纹理了,当然,这个只是本人处理前后置摄像头的一种简便方法。
下面我们再选择一个滤镜看看是如何做滤镜叠加的,我们回到CameraGLRendererBase的onSurfaceChanged和onDrawFrame回调中
/**
* GLSurfaceView.Renderer 回调接口,比如横竖屏切换
* @param gl
* @param surfaceWidth
* @param surfaceHeight
*/
@Override
public void onSurfaceChanged(GL10 gl, int surfaceWidth, int surfaceHeight) {
Log.i(TAG, "onSurfaceChanged ( " + surfaceWidth + " x " + surfaceHeight + ")");
mHaveSurface = true;
//更新surface状态
updateState();
//设置预览界面大小
setPreviewSize(surfaceWidth, surfaceHeight);
//设置OPENGL视窗大小及位置
GLES20.glViewport(0, 0, surfaceWidth, surfaceHeight);
//创建滤镜帧缓存的数据
filterGroup.onFilterChanged(surfaceWidth, surfaceHeight);
}
/**
* GLSurfaceView.Renderer 回调接口, 每帧更新
* @param gl
*/
@Override
public void onDrawFrame(GL10 gl) {
if (!mHaveFBO) {
return;
}
synchronized(this) {
//mUpdateST这个值设置是在每次有新的数据帧上来的时候设置为true,
//我们需要从图像中提取最近一帧,然后可以设置值为false,每次来新的帧数据调用一次
if (mUpdateST) {
//更新纹理图像为从图像流中提取的最近一帧
mSurfaceTexture.updateTexImage();
mUpdateST = false;
}
//OES是原始的摄像头数据纹理,然后再添加滤镜纹理
//N+1个滤镜(其中第一个从外部纹理接收的无滤镜效果)
filterGroup.onDrawFrame(oesFilter.getTextureId());
}
}
/**
*初始化SurfaceTexture并监听回调
*/
private void initSurfaceTexture() {
Log.d(TAG, "initSurfaceTexture");
deleteSurfaceTexture();
mSurfaceTexture = new SurfaceTexture(oesFilter.getTextureId());
mSurfaceTexture.setOnFrameAvailableListener(this);
}
我们主要看filterGroup.onFilterChanged和filterGroup.onDrawFrame()函数,onFilterChanged函数是创建滤镜帧缓存的数据,后者是真正的渲染滤镜效果,它传入的参数是oesFilter.getTextureId()的纹理,我们分别来看下到底做了什么事情:
进入filterGroup的onFilterChanged中,我们创建了帧缓冲来渲染纹理,对于SurfaceTexture,它是一个GL_TEXTURE_EXTERNAL_OES外部纹理,要想渲染相机预览到GL_TEXTURE_2D纹理上,唯一办法是采用帧缓冲FBO对象,可以将预览图像的外部纹理渲染到FBO的纹理中,剩下的滤镜再绑定到该纹理,这样的达到滤镜实现目的,详细我们看看注释:
/**
* 创建帧缓冲,bind数据
* 创建帧缓冲对象:(目前,帧缓冲对象N为1)
* 有N+1个滤镜(其中第一个从外部纹理接收的无滤镜效果),就需要分配N个帧缓冲对象,
* 首先创建大小为N的两个数组mFrameBuffers和mFrameBufferTextures,分别用来存储缓冲区id和纹理id,
* 通过GLES20.glGenFramebuffers(1, mFrameBuffers, i)来创建帧缓冲对象
*
* 对于SurfaceTexture,它是一个GL_TEXTURE_EXTERNAL_OES外部纹理,要想渲染相机预览到GL_TEXTURE_2D纹理上,
* 唯一办法是采用帧缓冲FBO对象,可以将预览图像的外部纹理渲染到FBO的纹理中,
* 剩下的滤镜再绑定到该纹理,这样的达到滤镜实现目的
*
* @param surfaceWidth
* @param surfaceHeight
*/
@Override
public void onFilterChanged(int surfaceWidth, int surfaceHeight) {
super.onFilterChanged(surfaceWidth, surfaceHeight);
//由于相机滤镜就是OES原始数据+滤镜效果组成的,所以这个size永远是等于2的
int size = filters.size();
for (int i = 0; i < size; i++){
filters.get(i).onFilterChanged(surfaceWidth, surfaceHeight);
}
if (FBO != null) {
//如果帧缓存存在先前数据,先清除帧缓冲
deleteFBO();
}
if (FBO == null) {
FBO = new int[size - 1];
texture = new int[size - 1];
/**
* 依次绘制:
* 首先第一个一定是绘制与SurfaceTexture绑定的外部纹理处理后的无滤镜效果,之后的操作与第一个一样,都是绘制到纹理。
* 首先与之前相同传入纹理id,并重新绑定到对应的缓冲区对象GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFrameBuffers[i]),
* 之后draw对应的纹理id。若不是最后一个滤镜,需要解绑缓冲区,下一个滤镜的新的纹理id即上一个滤镜的缓冲区对象所对应的纹理id,
* 同样执行上述步骤,直到最后一个滤镜。
*/
for (int i = 0; i < size - 1; i++) {
//创建帧缓冲对象
GLES20.glGenFramebuffers(1, FBO, i);
//创建纹理,当把一个纹理附着到FBO上后,所有的渲染操作就会写入到该纹理上,意味着所有的渲染操作会被存储到纹理图像上,
//这样做的好处是显而易见的,我们可以在着色器中使用这个纹理。
GLES20.glGenTextures(1, texture, i);
//bind纹理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture[i]);
//创建输出纹理,方法基本相同,不同之处在于glTexImage2D最后一个参数为null,不指定数据指针。
//使用了glTexImage2D函数,使用GLUtils#texImage2D函数加载一幅2D图像作为纹理对象,
//这里的glTexImage2D稍显复杂,这里重要的是最后一个参数,
//如果为null就会自动分配可以容纳相应宽高的纹理,然后后续的渲染操作就会存储到这个纹理上了。
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, surfaceWidth, surfaceHeight, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);
//指定纹理格式
//设置环绕方向S,截取纹理坐标到[1/2n,1-1/2n]。将导致永远不会与border融合
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
//设置环绕方向T,截取纹理坐标到[1/2n,1-1/2n]。将导致永远不会与border融合
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
//设置缩小过滤为使用纹理中坐标最接近的一个像素的颜色作为需要绘制的像素颜色
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
//设置放大过滤为使用纹理中坐标最接近的若干个颜色,通过加权平均算法得到需要绘制的像素颜色
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST);
//绑定帧缓冲区,第一个参数是target,指的是你要把FBO与哪种帧缓冲区进行绑定,此时创建的帧缓冲对象其实只是一个“空壳”,
//它上面还包含一些附着,因此接下来还必须往它里面添加至少一个附着才可以,
// 使用创建的帧缓冲必须至少添加一个附着点(颜色、深度、模板缓冲)并且至少有一个颜色附着点。
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, FBO[i]);
/**
* 函数将2D纹理附着到帧缓冲对象
* glFramebufferTexture2D()把一幅纹理图像关联到一个FBO,第二个参数是关联纹理图像的关联点,一个帧缓冲区对象可以有多个颜色关联点0~n
* 第三个参数textureTarget在多数情况下是GL_TEXTURE_2D。第四个参数是纹理对象的ID号
* 最后一个参数是要被关联的纹理的mipmap等级 如果参数textureId被设置为0,那么纹理图像将会被从FBO分离
* 如果纹理对象在依然关联在FBO上时被删除,那么纹理对象将会自动从当前帮的FBO上分离。然而,如果它被关联到多个FBO上然后被删除,
* 那么它将只被从绑定的FBO上分离,而不会被从其他非绑定的FBO上分离。
*/
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, texture[i], 0);
//现在已经完成了纹理的加载,不需要再绑定此纹理了解绑纹理对象
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
//解绑帧缓冲对象
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
}
}
Log.d(TAG, "initFBO error status: " + GLES20.glGetError());
//在完成所有附着的添加后,需要使用函数glCheckFramebufferStatus函数检查帧缓冲区是否完整
int FBOstatus = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER);
if (FBOstatus != GLES20.GL_FRAMEBUFFER_COMPLETE) {
Log.e(TAG, "initFBO failed, status: " + FBOstatus);
}
}
进入filterGroup的onDrawFrame中,我们再onFilterChanged中已经创建了FBO,创建了FBO空壳后,然后绑定FBO,然后创建纹理,绑定纹理,最后利用glFramebufferTexture2D函数,把纹理texture附着在这个壳子当中,在onDrawFrame中,我们把原始的OES外部纹理通过FBO,即GL_TEXTURE_EXTERNAL_OES转换为GL_TEXTURE_2D纹理,然后把上一个纹理传递给下一个纹理,这里一共就只有两个滤镜,第一个为OesFileter,就从摄像头传递过来的数据,我们通过OPENGL的顶点和片段着色器渲染后,再把这个渲染的纹理当做参数传递给下一个纹理处理,注释中很清楚了,下面我们将选择几个filter来加深下理解。
@Override
public void onDrawFrame(int textureId) {
//从链表中取出filter然后运行,在切换滤镜的时候运行,执行完后链表长度为0
runPreDrawTasks();
if (FBO == null || texture == null) {
return ;
}
int size = filters.size();
//oes无滤镜效果的纹理
int previousTexture = textureId;
for (int i = 0; i < size; i++) {
AbstractFilter filter = filters.get(i);
Log.d(TAG, "onDrawFrame: " + i + " / " + size + " "
+ filter.getClass().getSimpleName() + " "
+ filter.surfaceWidth + " " + filter.surfaceHeight);
if (i < size - 1) {
//先draw oesfilter中无滤镜效果的纹理,SurfaceTexture属于GL_TEXTURE_EXTERNAL_OES纹理
//注意OpengES FBO 把GL_TEXTURE_EXTERNAL_OES转换为GL_TEXTURE_2D,即OES外部纹理转化为了GL_TEXTURE_2D内部纹理,
//然后多个GL_TEXTURE_2D纹理叠加达到滤镜效果
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, FBO[i]);
filter.onDrawFrame(previousTexture);
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
//下一个滤镜的新的纹理id即上一个滤镜的缓冲区对象所对应的纹理id
previousTexture = texture[i];
} else {
//draw滤镜纹理
filter.onDrawFrame(previousTexture);
}
}
}
我们以ImageGradualFilter滤镜为例子,具体的接口的实现跟OESFilter实现差不多,都是实现了AbstractFilter这个基类
/**
* ImageGradualFilter滤镜为指定的图片作为纹理与摄像头纹理做强光算法处理
*/
public class ImageGradualFilter extends AbstractFilter{
private static final String TAG = "ImageGradualFilter";
private Context mContext;
private int[] mTexture = new int[1];
private String filterVs, filterFs;
private int prog2D = -1;
private int vPos2D, vTC2D;
private final int resId;
private final int index;
/**
* 这个滤镜是两张图片的纹理叠加,具体叠加算法见顶点着色器和片段着色器
* @param context
* @param resId 纹理图片资源id
* @param index 滤镜索引
*/
public ImageGradualFilter(Context context, int resId, int index) {
super(TAG);
mContext = context;
this.resId = resId;
this.index = index;
}
不同的是初始化的时候增加了一个纹理,这个纹理是我们自己的资源图片,
@Override
public void init() {
//初始化着色器
initShader(resId);
}
private void initShader(int resId){
//根据资源id生成纹理
genTexture(resId);
if (index <= 9) {
//读取顶点着色器字段
filterVs = TextResourceReader.readTextFileFromResource(mContext, R.raw.origin_vs);
//读取片段着色器字段
filterFs = TextResourceReader.readTextFileFromResource(mContext, R.raw.filter_gradual_fs);
} else if (index == 10) {
filterVs = TextResourceReader.readTextFileFromResource(mContext, R.raw.origin_vs);
filterFs = TextResourceReader.readTextFileFromResource(mContext, R.raw.filter_lomo_fs);
} else if (index == 11) {
filterVs = TextResourceReader.readTextFileFromResource(mContext, R.raw.origin_vs);
filterFs = TextResourceReader.readTextFileFromResource(mContext, R.raw.filter_lomo_yellow_fs);
}
//载入顶点着色器和片段着色器
prog2D = Util.loadShader(filterVs, filterFs);
//获取顶点着色器中attribute location属性vPosition, vTexCoord
vPos2D = GLES20.glGetAttribLocation(prog2D, "vPosition");
vTC2D = GLES20.glGetAttribLocation(prog2D, "vTexCoord");
//开启顶点属性数组
GLES20.glEnableVertexAttribArray(vPos2D);
GLES20.glEnableVertexAttribArray(vTC2D);
}
看看我们资源图片如何加载到纹理当中,使用GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);这个函数,生成了bitmap的纹理映射,这个纹理的属性是GLES20.GL_TEXTURE_2D的,前面已经说了,GL_TEXTURE_EXTERNAL_OES的外部纹理通过FBO转化为了GLES20.GL_TEXTURE_2D的内部纹理,然后通过这个bitmap的GL_TEXTURE_2D纹理做叠加处理
/**
* 通过资源id获取bitmap,然后转化为纹理
* @param resId
*/
private void genTexture(int resId) {
//生成纹理
GLES20.glGenTextures(1, mTexture, 0);
//加载Bitmap
Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), resId);
if (bitmap != null) {
//glBindTexture允许我们向GLES20.GL_TEXTURE_2D绑定一张纹理
//当把一张纹理绑定到一个目标上时,之前对这个目标的绑定就会失效
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTexture[0]);
//设置纹理映射的属性
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
//如果bitmap加载成功,则生成此bitmap的纹理映射
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
//释放bitmap资源
bitmap.recycle();
}
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,0);
}
我们再来看看onDrawFrame是如何处理的,textureId参数为上一次纹理,然后我们再来绑定这个bitmap纹理,最后通过顶点和片段着色器来做处理,这里我们激活了两个纹理,GLES20.GL_TEXTURE0和GLES20.GL_TEXTURE1,显然,这两个纹理会在着色器有所体现:
@Override
public void onDrawFrame(int textureId) {
onPreDrawElements();
//设置窗口可视区域
GLES20.glViewport(0, 0, surfaceWidth, surfaceHeight);
//激活纹理,当有多个纹理的时候,可以依次递增GLES20.GL_TEXTURE
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
//绑定纹理,textureId为FBO处理完毕后的内部纹理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
//设置sampler2D"sTexture1"到纹理 unit 0
GLES20.glUniform1i(GLES20.glGetUniformLocation(prog2D, "sTexture1"), 0);
//绑定纹理,mTexture[0]为加载的纹理图片,两个纹理做叠加
GLES20.glActiveTexture(GLES20.GL_TEXTURE1);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTexture[0]);
//设置sampler2D"sTexture1"到纹理 unit 1
GLES20.glUniform1i(GLES20.glGetUniformLocation(prog2D, "sTexture2"), 1);
draw();
}
好,我们来看看滤镜的核心在哪里,OPENGL只不过是我们的显示手段,最重要的内容还是要看两个着色器:
顶点着色器origin_vs.txt,只是显示顶点,我们没有对顶点做什么计算
attribute vec2 vPosition;
attribute vec2 vTexCoord;
varying vec2 texCoord;
void main() {
texCoord = vTexCoord;
gl_Position = vec4 ( vPosition.x, vPosition.y, 0.0, 1.0 );
}
filter_gradual_fs.txt片段着色器:分别把两个纹理转换为RGB的值,然后分别对RGB做BlendHardLight的算法,算法已经列出,详细见下面
precision mediump float;
uniform sampler2D sTexture1;
uniform sampler2D sTexture2;
varying vec2 texCoord;
float BlendMultiply(float baseColor, float blendColor) {
return baseColor * blendColor;
}
float BlendHardLight(float baseColor, float blendColor) {
if (blendColor < 0.5) {
return 2.0 * baseColor * blendColor;
} else {
return (1.0 - 2.0 * (1.0 - baseColor) * (1.0 - blendColor));
}
}
void main() {
//gl_FragColor = texture2D(sTexture, texCoord);
//gl_FragColor = mix(texture2D(sTexture1, texCoord), texture2D(sTexture2, texCoord), 0.4);
vec3 baseColor = texture2D(sTexture1, texCoord).rgb;
vec3 color = texture2D(sTexture2, texCoord).rgb;
float r = BlendHardLight(baseColor.r, color.r);
float g = BlendHardLight(baseColor.g, color.g);
float b = BlendHardLight(baseColor.b, color.b);
gl_FragColor = vec4(r, g, b, 1.0);
}
所有的滤镜的顶点着色器和片段着色器都以txt的文件形式存在,路径再R.raw中
里面提供了demo的所有滤镜算法,详细请看里面的内容,大家可以去github下载,列举几个片段着色器内容:
filter_lomo_fs.txt
precision mediump float;
uniform sampler2D sTexture1;
uniform sampler2D sTexture2;
varying vec2 texCoord;
float BlendMultiply(float baseColor, float blendColor) {
return baseColor * blendColor;
}
float BlendHardLight(float baseColor, float blendColor) {
if (blendColor < 0.5) {
return 2.0 * baseColor * blendColor;
} else {
return (1.0 - 2.0 * (1.0 - baseColor) * (1.0 - blendColor));
}
}
void main() {
vec3 baseColor = texture2D(sTexture1, texCoord).rgb;
vec3 color = texture2D(sTexture2, texCoord).rgb;
float r = BlendMultiply(BlendHardLight(baseColor.r, baseColor.r), color.r);
float g = BlendMultiply(BlendHardLight(baseColor.g, baseColor.g), color.g);
float b = BlendMultiply(BlendHardLight(baseColor.b, baseColor.b), color.b);
gl_FragColor = vec4(r, g, b, 1.0);
}
filter_lomo_yellow_fs.txt
precision mediump float;
uniform sampler2D sTexture1;
uniform sampler2D sTexture2;
varying vec2 texCoord;
float BlendMultiply(float baseColor, float blendColor) {
return baseColor * blendColor;
}
void main() {
vec3 baseColor = texture2D(sTexture1, texCoord).rgb;
vec3 color = texture2D(sTexture2, texCoord).rgb;
float r = BlendMultiply(baseColor.r, color.r);
float g = BlendMultiply(baseColor.g, color.g);
float b;
if(baseColor.b < 0.2) {
b = 0.0;
} else {
b = BlendMultiply(baseColor.b - 0.2, color.b);
}
gl_FragColor = vec4(r , g, b, 1.0);
}
filter_texture_fs.txt
precision mediump float;
uniform sampler2D sTexture1;
uniform sampler2D sTexture2;
varying vec2 texCoord;
float BlendOverLay(float baseColor, float blendColor) {
if(baseColor < 0.5) {
return 2.0 * baseColor * blendColor;
}
else {
return 1.0 - ( 2.0 * ( 1.0 - baseColor) * ( 1.0 - blendColor));
}
}
void main() {
vec3 baseColor = texture2D(sTexture1, texCoord).rgb;
vec3 blendColor = texture2D(sTexture2, texCoord).rgb;
float r = BlendOverLay(baseColor.r, blendColor.r);
float g = BlendOverLay(baseColor.g, blendColor.g);
float b = BlendOverLay(baseColor.b, blendColor.b);
gl_FragColor = vec4(r, g, b, 1.0);
}
现在我们讲完了如何使用OPENGL来显示滤镜,下面我们再看看图片如何使用RenderScirpt来处理大图,显然,OPENGL只是给我们提供显示渲染的功能,我们如何把我们所看到的滤镜保存到图片中取呢?
二. 使用RenderScript处理拍照后的图片
我们先来了解一下什么是RenderScript,RenderScript是安卓平台上很受谷歌推荐的一个高效计算平台,它能够自动把计算任务分配到各个可用的计算核心上,包括CPU,GPU以及DSP等,提供十分高效的并行计算能力。
使用了RenderScript的应用与一般的安卓应用在代码编写上与并没有太大区别。使用了RenderScript的应用依然像传统应用一样运行在VM中,但是你需要给你的应用编写你所需要的RenderScript代码,且这部分代码运行在native层。
RenderScript采用从属控制架构:底层RenderScript被运行在虚拟机中的上层安卓系统所控制。安卓VM负责所有内存管理并把它分配给RenderScript的内存绑定到RenderScript运行时,所以RenderScript代码能够访问这些内存。安卓框架对RenderScript进行异步调用,每个调用都放在消息队列中,并且会被尽快处理。
我们需要先编写RenderScript文件
RenderScript代码放在.rs或者.rsh文件中,在RenderScript代码中包含计算逻辑以及声明所有必须的变量和指针,通常一个.rs文件包含如下几个部分:
我们还是举几个例子
filter_gradual_color.rs,即把两个Allocation数据传递进来,v_color就是资源图片,v_out就是拍照后的原始图片,转化为RS脚本知道的uchar4*数据,然后调用rsUnpackColor8888,把数据转化为float4的rgba四通道,这里的算法只取rgb三通道,然后分别把r,g,b做BlendHardLight算法,当blendColor小于0.5,即类似于0~255中value为128的时候,做正片叠底算法,否则,就做滤色算法。
#pragma version(1)
#pragma rs java_package_name(com.riemann.camera)
#include "utils.rsh"
static float BlendHardLight(float baseColor, float blendColor) {
if (blendColor < 0.5) {
return 2.0 * baseColor * blendColor;
} else {
return (1.0 - 2.0 * (1.0 - baseColor) * (1.0 - blendColor));
}
}
void root(const uchar4 *v_color, uchar4 *v_out, uint32_t x, uint32_t y) {
//unpack a color to a float4
float4 f4 = rsUnpackColor8888(*v_color);
float3 color1 = f4.rgb;
float3 color2 = rsUnpackColor8888(*v_out).rgb;
float r = BlendHardLight(color2.r, color1.r);
float g = BlendHardLight(color2.g, color1.g);
float b = BlendHardLight(color2.b, color1.b);
float3 color;
color.r = r;
color.g = g;
color.b = b;
color = clamp(color, 0.0f, 1.0f);
*v_out = rsPackColorTo8888(color);
}
大家看看是不是跟片段着色器非常相似呢?当创建了filter_gradual_color.rs脚本后,相应的AndroidStudio会自动生成ScriptC_filter_gradual_color这个类,
ScriptC_filter_gradual_color,我们看看CameraPhotoRS构造函数,
让我介绍一下上面代码中使用到的三个重要的对象:
Allocation: 内存分配是在java端完成的因此你不应该在每个像素上都要调用的函数中malloc。我创建的第一个allocation是用bitmap中的数据装填的。第二个没有初始化,它包含了一个与第一个allocation的大小和type都相同多2D数组。
Type: “一个Type描述了 一个Allocation或者并行操作的Element和dimensions ” (摘自 developer.android.com)
Element: “一个 Element代表一个Allocation内的一个item。一个 Element大致相当于RenderScript kernel里的一个c类型。Elements可以简单或者复杂” (摘自 developer.android.com)
public CameraPhotoRS(Context context){
mContext = context;
mRS = RenderScript.create(context);
mFilterGary = new ScriptC_filter_gary(mRS);
mFilterAnsel = new ScriptC_filter_ansel(mRS);
mFilterSepia = new ScriptC_filter_sepia(mRS);
mFilterRetro = new ScriptC_filter_retro(mRS);
mFilterGeorgia = new ScriptC_filter_georgia(mRS);
mFilterSahara = new ScriptC_filter_sahara(mRS);
mFilterPolaroid = new ScriptC_filter_polaroid(mRS);
mFilterDefault = new ScriptC_filter_default(mRS);
mFilterGradualColor = new ScriptC_filter_gradual_color(mRS);
mFilterGradualColorDefault = new ScriptC_filter_gradual_color_default(mRS);
mFilterLomo = new ScriptC_filter_lomo(mRS);
mFilterLomoYellow = new ScriptC_filter_lomo_yellow(mRS);
mFilterTexture = new ScriptC_filter_texture(mRS);
mFilterRetro2 = new ScriptC_filter_retro2(mRS);
mFilterStudio = new ScriptC_filter_studio(mRS);
scriptIntrinsicBlend = ScriptIntrinsicBlend.create(mRS, Element.U8_4(mRS));
mFilterCarv = new ScriptC_filter_carv(mRS);
}
对RenderScript的处理在applyFilter中,bitmapIn是拍照生成的图片,我们通过Allocation.createFromBitmap的接口转化为RS识别的Allocation,我们还是看看ScriptC_filter_gradual_color如何处理的,下面的代码case 2中,我们先加载一个资源到Bitmap中,然后生成RS能识别的allocationCutter,然后调用mFilterGradualColor.forEach_root,这样就到了上面的filter_gradual_color.rs中处理,最后返回的就是我们用RS渲染过的图片。RS渲染脚本的效率非常高
public void applyFilter(Bitmap bitmapIn, int index) {
Allocation inAllocation = Allocation.createFromBitmap(mRS, bitmapIn,
Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT);
switch (index) {
case 1: {
mFilterGary.forEach_root(inAllocation);
break;
}
case 2: {
Bitmap mBitmapCutter = getCutterBitmap(bitmapIn.getWidth(), bitmapIn.getHeight(), R.drawable.change_rainbow);
Allocation allocationCutter = Allocation.createFromBitmap(mRS, mBitmapCutter,
Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT);
//scriptIntrinsicBlend.forEachDstIn(inAllocation, allocationCutter);
//scriptIntrinsicBlend.forEachSrcAtop(allocationCutter, inAllocation);
mFilterGradualColor.forEach_root(allocationCutter, inAllocation);
allocationCutter.copyTo(mBitmapCutter);
allocationCutter.destroy();
break;
}
我们再看个例子
filter_lomo.rs,这个算法我们看是咋样处理的
float r = BlendMultiply(BlendHardLight(color2.r, color2.r), color1.r);
先把color2做强光处理,然后再与color1做正片叠底处理,其实算法比较简单,这样,对于给定的图片,我们就可以实现相应的滤镜了
#pragma version(1)
#pragma rs java_package_name(com.riemann.camera)
#include "utils.rsh"
static float BlendHardLight(float baseColor, float blendColor) {
if (blendColor < 0.5) {
return 2.0 * baseColor * blendColor;
} else {
return (1.0 - 2.0 * (1.0 - baseColor) * (1.0 - blendColor));
}
}
static float BlendMultiply(float baseColor, float blendColor) {
return baseColor * blendColor;
}
void root(const uchar4 *v_color, uchar4 *v_out, uint32_t x, uint32_t y) {
//unpack a color to a float4
float4 f4 = rsUnpackColor8888(*v_color);
float3 color1 = f4.rgb;
float3 color2 = rsUnpackColor8888(*v_out).rgb;
float r = BlendMultiply(BlendHardLight(color2.r, color2.r), color1.r);
float g = BlendMultiply(BlendHardLight(color2.g, color2.g), color1.g);
float b = BlendMultiply(BlendHardLight(color2.b, color2.b), color1.b);
float3 color;
color.r = r;
color.g = g;
color.b = b;
color = clamp(color, 0.0f, 1.0f);
*v_out = rsPackColorTo8888(color);
}
好了,我们列举了两个RenderScirpt处理图片的例子,要看其它例子,可以下载demo去看看,由于Camera不是本文的重点,所以对于预览的处理,没有设置自动对焦,还没有达到很好的效果,等有时间了再更新demo。
最后给大家看看处理后的图片效果:
工程地址:https://github.com/liweiping1314521/RiemannCamera/
除此之外,android也给我们提供了很多RenderScript的类,比如
比如我们可以使用ScriptIntrinsicBlend这个类来实现正片叠底,这个类里面有很多函数,可以实现两张图片的叠加,又比如ScriptIntrinsicBlur这个显示了高斯模糊,比如我们实现毛玻璃效果,用这个类就可以了,这给我们的图片处理带来了极大的方便,我们直接拿过来使用就行,而不用我们再写个jni,RenderScript的处理效率远比我们自己处理来的快。
好了,终于说完了如何使用OPENGL滤镜和用RenderScript来处理图片,由于本人水平有限,难免会有说错的地方,希望大家批评指正,一起学习,共同进步。
参考文章:
https://blog.csdn.net/zhuiqiuk/article/details/54728431
https://blog.csdn.net/oshunz/article/details/50176901
https://blog.csdn.net/junzia/article/details/53861519