Android中OpenGL濾鏡和RenderScript圖片處理

RiemannLee發表於2018-03-28

前言:以前做過一個相機,當時使用的是OpenCV庫來進行濾鏡和圖片的處理,當時發現濾鏡處理的時間比較長,實時性還有待進一步提高,對於使用NDK對camera處理每一幀,演算法必須要非常優化和簡單,對於一些複雜演算法,處理時間比較長的,就不太適合實時處理的濾鏡,那麼我們該怎麼優化相機的濾鏡和儲存拍照的圖片呢?當然是使用OpenGL和RS渲染指令碼,比起使用ndk來處理每一幀的圖片,OpenGL和RenderScript指令碼處理的相當快,它們的運算都是使用GPU去渲染的,而OpenCV的處理速度取決於CPU的執行速度,下面我們將分別來介紹下如何使用OpenGL和RenderScript來渲染圖片流。

工程地址: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中的頂點座標與紋理座標的關係

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

相關文章