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

相關文章
相關標籤/搜索