Android中OpenGL滤镜和RenderScript图片处理

前言:之前作过一个相机,当时使用的是OpenCV库来进行滤镜和图片的处理,当时发现滤镜处理的时间比较长,实时性还有待进一步提升,对于使用NDK对camera处理每一帧,算法必需要很是优化和简单,对于一些复杂算法,处理时间比较长的,就不太适合实时处理的滤镜,那么咱们该怎么优化相机的滤镜和保存拍照的图片呢?固然是使用OpenGL和RS渲染脚本,比起使用ndk来处理每一帧的图片,OpenGL和RenderScript脚本处理的至关快,它们的运算都是使用GPU去渲染的,而OpenCV的处理速度取决于CPU的执行速度,下面咱们将分别来介绍下如何使用OpenGL和RenderScript来渲染图片流。html

工程地址:RiemannCamerajava

一. 下面咱们来看看整个摄像头如何用OPENGL处理滤镜的: 先看一下类的关系图:android

图片的标注

先来看看CameraGLSurfaceView这个类git

/**
 * 咱们使用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外部纹理,第二层是咱们选择的滤镜:github

/**
 * 显示滤镜的核心类
 */
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的这几个函数bash

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中的顶点坐标与纹理坐标的关系

image.png
咱们再绘制顶点坐标的时候
image.png
顶点坐标跟纹理坐标
image.png
看了上面的解释,咱们就知道为啥要这样显示纹理了,固然,这个只是本人处理先后置摄像头的一种简便方法。

下面咱们再选择一个滤镜看看是如何作滤镜叠加的,咱们回到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中

image.png
里面提供了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进行异步调用,每一个调用都放在消息队列中,而且会被尽快处理。

image.png

咱们须要先编写RenderScript文件 RenderScript代码放在.rs或者.rsh文件中,在RenderScript代码中包含计算逻辑以及声明全部必须的变量和指针,一般一个.rs文件包含以下几个部分:

image.png

咱们仍是举几个例子 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构造函数, 让我介绍一下上面代码中使用到的三个重要的对象:

  1. Allocation: 内存分配是在java端完成的所以你不该该在每一个像素上都要调用的函数中malloc。我建立的第一个allocation是用bitmap中的数据装填的。第二个没有初始化,它包含了一个与第一个allocation的大小和type都相同多2D数组。

  2. Type: “一个Type描述了 一个Allocation或者并行操做的Element和dimensions ” (摘自 developer.android.com)

  3. 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。

最后给你们看看处理后的图片效果:

image.png

工程地址:RiemannCamera

除此以外,android也给咱们提供了不少RenderScript的类,好比

image.png

好比咱们可使用ScriptIntrinsicBlend这个类来实现正片叠底,这个类里面有不少函数,能够实现两张图片的叠加,又好比ScriptIntrinsicBlur这个显示了高斯模糊,好比咱们实现毛玻璃效果,用这个类就能够了,这给咱们的图片处理带来了极大的方便,咱们直接拿过来使用就行,而不用咱们再写个jni,RenderScript的处理效率远比咱们本身处理来的快。

好了,终于说完了如何使用OPENGL滤镜和用RenderScript来处理图片,因为本人水平有限,不免会有说错的地方,但愿你们批评指正,一块儿学习,共同进步。

同步发布于:https://www.jianshu.com/p/66d0fcb902ab

参考文章:

https://blog.csdn.net/zhuiqiuk/article/details/54728431

https://blog.csdn.net/oshunz/article/details/50176901

https://blog.csdn.net/junzia/article/details/53861519

相关文章
相关标签/搜索